package com.saml.demo.sp.controller;

import com.alibaba.fastjson.JSON;
import com.saml.demo.sp.constants.SPConstants;
import com.saml.demo.sp.utils.OpenSAMLUtils;
import com.saml.demo.sp.utils.SPCredentials;
import com.saml.demo.sp.vo.UserAccountVo;
import lombok.extern.log4j.Log4j;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.httpclient.HttpClientBuilder;
import org.joda.time.DateTime;
import org.opensaml.messaging.context.InOutOperationContext;
import org.opensaml.messaging.context.MessageContext;
import org.opensaml.messaging.encoder.MessageEncodingException;
import org.opensaml.messaging.pipeline.httpclient.BasicHttpClientMessagePipeline;
import org.opensaml.messaging.pipeline.httpclient.HttpClientMessagePipeline;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.saml.common.SAMLObject;
import org.opensaml.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler;
import org.opensaml.saml.common.messaging.context.SAMLEndpointContext;
import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.binding.decoding.impl.HttpClientResponseSOAP11Decoder;
import org.opensaml.saml.saml2.binding.encoding.impl.HTTPRedirectDeflateEncoder;
import org.opensaml.saml.saml2.binding.encoding.impl.HttpClientRequestSOAP11Encoder;
import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.metadata.Endpoint;
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
import org.opensaml.soap.client.http.AbstractPipelineHttpSOAPClient;
import org.opensaml.soap.common.SOAPException;
import org.opensaml.xmlsec.SignatureSigningParameters;
import org.opensaml.xmlsec.context.SecurityParametersContext;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 用户账户信息控制器
 * Created by Derry Luo on 2019/5/8.
 */
@RequestMapping("/sp/securesource/account")
@Controller
@Log4j
public class AccountController {

    /**
     * 注销登录
     *
     * @param request
     */
    @RequestMapping("logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {

        // 从session中获取NameId
        UserAccountVo userAccountVo = (UserAccountVo) request.getSession().getAttribute(SPConstants.USER_ACCOUNT);
        log.info("【User logout】userAccountVo = " + JSON.toJSONString(userAccountVo));
        // 构建logoutRequest
        LogoutRequest logoutRequest = this.buildLogoutRequest(userAccountVo.getNameId());

        // 重定向到IdP注销页面
        this.redirectUserWithRequest(response, logoutRequest);

        return "/index";
    }

    /**
     * 使用SOAP协议发送 ArtifactResolve
     */
    private LogoutResponse sendAndReceiveLogoutRequest(final LogoutRequest logoutRequest) {
        try {

            MessageContext<LogoutRequest> contextout = new MessageContext<LogoutRequest>();

            contextout.setMessage(logoutRequest);
            //加入数据签名以增强安全性
            SignatureSigningParameters signatureSigningParameters = new SignatureSigningParameters();
            signatureSigningParameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
            signatureSigningParameters.setSigningCredential(SPCredentials.getCredential());
            signatureSigningParameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);

            SecurityParametersContext securityParametersContext = contextout.getSubcontext(SecurityParametersContext.class, true);
            if (securityParametersContext != null) {
                securityParametersContext.setSignatureSigningParameters(signatureSigningParameters);
            }

            //创建InOutOperationContext来处理输入输出的信息
            InOutOperationContext<LogoutResponse, LogoutRequest> context = new ProfileRequestContext<LogoutResponse, LogoutRequest>();
            context.setOutboundMessageContext(contextout);


            //为了能发送SOAP消息，还需要设置SOAP Client。
            // 这个Client将会调用消息的处理器，编码器以及解码等来传送消息
            AbstractPipelineHttpSOAPClient<SAMLObject, SAMLObject> soapClient = new AbstractPipelineHttpSOAPClient<SAMLObject, SAMLObject>() {
                @Nonnull
                protected HttpClientMessagePipeline newPipeline() throws SOAPException {
                    //创建输入输出用的编码器和解码器
                    HttpClientRequestSOAP11Encoder encoder = new HttpClientRequestSOAP11Encoder();
                    HttpClientResponseSOAP11Decoder decoder = new HttpClientResponseSOAP11Decoder();
                    //创建管道
                    BasicHttpClientMessagePipeline pipeline = new BasicHttpClientMessagePipeline(
                            encoder,
                            decoder
                    );
                    //为输出的内容签名
                    pipeline.setOutboundPayloadHandler(new SAMLOutboundProtocolMessageSigningHandler());
                    return pipeline;
                }
            };

            // HTTP帮助SOAPClient编码和解码
            HttpClientBuilder clientBuilder = new HttpClientBuilder();
//            HttpSOAPClient soapClient = new HttpSOAPClient();
//            soapClient.setParserPool(new BasicParserPool());
            soapClient.setHttpClient(clientBuilder.buildClient());

            soapClient.send(SPConstants.SSO_LOGOUT_SERVICE, context);

            return context.getInboundMessageContext().getMessage();
        } catch (SecurityException e) {
            throw new RuntimeException(e);
        } catch (ComponentInitializationException e) {
            throw new RuntimeException(e);
        } catch (MessageEncodingException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 重定向到IdP验证页面
     *
     * @param httpServletResponse
     * @param logoutRequest
     */
    private void redirectUserWithRequest(HttpServletResponse httpServletResponse, LogoutRequest logoutRequest) {

        MessageContext context = new MessageContext();

        context.setMessage(logoutRequest);

        //关于传输对端实体的信息，对于IDP就是SP，对于SP就是IDP；
        SAMLPeerEntityContext peerEntityContext =
                context.getSubcontext(SAMLPeerEntityContext.class, true);

        //端点信息；
        SAMLEndpointContext endpointContext = peerEntityContext.getSubcontext(SAMLEndpointContext.class, true);
        endpointContext.setEndpoint(getIPDEndpoint());

        //数据签名环境上线文
        SignatureSigningParameters signatureSigningParameters = new SignatureSigningParameters();
        // TODO 获得证书，其中包含公钥。证书由IdP颁发
        signatureSigningParameters.setSigningCredential(SPCredentials.getCredential());
        //ALGO_ID_SIGNATURE_RSA_SHA256
        signatureSigningParameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);


        context.getSubcontext(SecurityParametersContext.class, true)
                .setSignatureSigningParameters(signatureSigningParameters);

        // OpenSAML提供了HTTPRedirectDefalteEncoder
        // 它将帮助我们来对于AuthnRequest进行序列化和签名
        HTTPRedirectDeflateEncoder encoder = new HTTPRedirectDeflateEncoder();

        encoder.setMessageContext(context);
        encoder.setHttpServletResponse(httpServletResponse);

        try {
            encoder.initialize();
        } catch (ComponentInitializationException e) {
            throw new RuntimeException(e);
        }

        log.info("LogoutRequest: ");
        OpenSAMLUtils.logSAMLObject(logoutRequest);

        log.info("Redirecting to IDP");
        try {
            //*encode*方法将会压缩消息，生成签名，添加结果到URL并从定向用户到Idp.
            //先使用RFC1951作为默认方法压缩数据，在对压缩后的<?xml version="1.0" encoding="UTF-8"?><saml2p:AuthnRequest xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://localhost:8080/webprofile-ref-project/sp/consumer" Destination="http://localhost:8080/webprofile-ref-project/idp/singleSignOnService" ID="_c8f4ccab841c698951a4cc63089519fd" IssueInstant="2019-05-03T06:07:17.398Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Version="2.0">
            //<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">TestSP</saml2:Issuer>
            //<saml2p:NameIDPolicy AllowCreate="true" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"/>
            //<saml2p:RequestedAuthnContext Comparison="minimum">
            //<saml2:AuthnContextClassRef xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml2:AuthnContextClassRef>
            //</saml2p:RequestedAuthnContext>
            //</saml2p:AuthnRequest>数据信息Base64编码
            encoder.encode();
        } catch (MessageEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private Endpoint getIPDEndpoint() {
        SingleSignOnService endpoint = OpenSAMLUtils.buildSAMLObject(SingleSignOnService.class);
        endpoint.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
        endpoint.setLocation(SPConstants.SSO_LOGOUT_SERVICE);

        return endpoint;
    }

    private LogoutRequest buildLogoutRequest(String nameId) {

        LogoutRequest logoutRequest = OpenSAMLUtils.buildSAMLObject(LogoutRequest.class);
        //请求时间：该对象创建的时间，以判断其时效性
        logoutRequest.setIssueInstant(new DateTime());
        //请求的ID：为当前请求设置ID，一般为随机数
        logoutRequest.setID(OpenSAMLUtils.generateSecureRandomId());
        //Issuer： 发行人信息，也就是SP的ID，一般是SP的URL
        logoutRequest.setIssuer(buildIssuer());
        // NameId是用户在IPD中的唯一标识
        logoutRequest.setNameID(buildNameId(nameId));

        return logoutRequest;
    }

    private NameID buildNameId(String nameId) {
        NameID nameID = OpenSAMLUtils.buildSAMLObject(NameID.class);
        nameID.setValue(nameId);
        return nameID;
    }

    private Issuer buildIssuer() {
        Issuer issuer = OpenSAMLUtils.buildSAMLObject(Issuer.class);
        issuer.setValue(getSPIssuerValue());

        return issuer;
    }

    private String getSPIssuerValue() {
        return SPConstants.SP_ENTITY_ID;
    }

}
